package ui;

import net.Poster;
import net.Poster.Result;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.DropMode;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

/**
 * This is the main presentation UI:
 * =============================================================|
 * |Options|First post|Later posts|Files|   |  Captcha input: 	|
 * |        ---------- ----------- -----    |  n remaining    	|
 * |  Name, Email, etc.: ________      		|					|
 * |         								| UNREADABLE 03132  |
 * |  Post delay, user agent, etc.: ____    |<CURRENT CAPTCHA  >|
 * |----------------------------------------|					|
 * | Progress: ******---------------------  |					|
 * | Log:									|					|
 * | NN:NN:NN whatever <click for details>  |		etc.		|
 * |										|					|
 * |	[Start/Pause]	[Cancel]			|					|
 * --------------------------------------------------------------
 */
public class CaptchasMoot extends javax.swing.JFrame implements Poster.Lifecycle {
	private final CaptchaScroller scroller;
	private final JButton startButton, cancelButton;
	private final List<Component> disabledWhenBusy = new ArrayList<Component>(); // These are the things that go insensitive while we're posting.  Only Cancel does the opposite.
	private final JProgressBar progressBar;
	private final JTextField boardOrThreadURL, userAgent, name, email, password;
	private Poster poster;
	private final DefaultStyledDocument log;
	private final JTextPane logPane;
	private final PostContentsArea firstPost, laterPosts;
	private CaptchasMoot() {
		super("CaptchasMoot Image Floodulator $Rev: 10 $");
		// URL address, String httpUserAgent, String name, String email, String password, String initialSubject, String initialBody, String subsequentSubject, String subsequentBody
		JTabbedPane tabs = new JTabbedPane();
		Box optionsTab = Box.createVerticalBox();
		boardOrThreadURL = addTextField(optionsTab, "Board or thread to post on", 0, "URL of the image board or specific thread.");
		userAgent = addTextField(optionsTab, "HTTP User-Agent", 0, "Set this to something prudent.");
		name = addTextField(optionsTab, "Name", 28, "User name (optional)");
		email = addTextField(optionsTab, "E-mail", 28, "E-mail address (optional)");
		password = addTextField(optionsTab, "Password", 8, "Password to use for post deletion.");
		addPostDelaySetting(optionsTab);
		optionsTab.add(Box.createVerticalGlue());
		tabs.addTab("Options", null, new JScrollPane(optionsTab), "Most of these fields are allowed to be blank.");
		
		firstPost = new PostContentsArea();
		tabs.addTab("First post", null, firstPost, "This subject and comment will be used only for the first post.");
		
		laterPosts = new PostContentsArea();
		tabs.addTab("Later posts", null, laterPosts, "This subject and comment will be used for posts after the first.");
		
		Box filesTab = makeFilesTab();
		tabs.addTab("Files", filesTab);
		
		Box progress = Box.createVerticalBox();
		Box progressBarArea = Box.createHorizontalBox();
		JLabel progressLabel = new JLabel("Progress:");
		progressLabel.setBorder(new EmptyBorder(30, 30, 30, 15));
		progressBarArea.add(progressLabel);
		progressBar = new JProgressBar(0, 1);
		progressBar.setBorder(new EmptyBorder(30, 15, 30, 30));
		progressBarArea.add(progressBar);
		progress.add(progressBarArea);
		log = new DefaultStyledDocument();
		logPane = new JTextPane(log);
		logPane.setEditable(false);
		progress.add(new JScrollPane(logPane));
		info("Started " + getTitle());
		Box controlButtons = Box.createHorizontalBox();
		startButton = new JButton(StartButtonActions.START);
		startButton.setEnabled(false);
		cancelButton = new JButton("Cancel");
		cancelButton.setEnabled(false);
		controlButtons.add(Box.createGlue());
		controlButtons.add(startButton);
		controlButtons.add(Box.createGlue());
		controlButtons.add(cancelButton);
		controlButtons.add(Box.createGlue());
		progress.add(controlButtons);
		Box tabsAndProgress = Box.createVerticalBox();
		tabsAndProgress.add(tabs);
		tabsAndProgress.add(progress);
		getContentPane().add(tabsAndProgress, BorderLayout.CENTER);
		
		scroller = CaptchaScroller.create();
		getContentPane().add(scroller, BorderLayout.EAST);
		connectEvents();
	}
	
	private JTextField postDelay;
	private void addPostDelaySetting(Box optionsTab) {
		String tooltip = "Enter a number of seconds to delay between posts.\nIf you hit the enter key, it will take effect immediately.";
		Box fieldArea = Box.createHorizontalBox();
		JLabel label = new JLabel("Delay between successful posts:");
		label.setBorder(new EmptyBorder(10, 20, 10, 20));
		label.setToolTipText(tooltip);
		fieldArea.add(label);
		postDelay = new JTextField(String.valueOf(Poster.DEFAULT_POST_DELAY), 10);
		postDelay.setToolTipText(tooltip);
		fieldArea.add(postDelay);
		fieldArea.add(Box.createHorizontalGlue());
		fieldArea.setBorder(new EmptyBorder(10, 0, 10, 0));
		optionsTab.add(fieldArea);
	}

	private JTextField addTextField(Box target, String name, int width,
			String tooltip) {
		Box fieldArea = Box.createHorizontalBox();
		JLabel label = new JLabel(name + ":");
		label.setBorder(new EmptyBorder(10, 20, 10, 20));
		label.setToolTipText(tooltip);
		fieldArea.add(label);
		JTextField field = new JTextField(width);
		field.setToolTipText(tooltip);
		fieldArea.add(field);
		fieldArea.setBorder(new EmptyBorder(10, 0, 10, 0));
		disabledWhenBusy.add(field);
		target.add(fieldArea);
		return field;
	}
	
	private class PostContentsArea extends Box {
		private final JTextField subject;
		private final JTextArea comment;
		public PostContentsArea() {
			super(BoxLayout.Y_AXIS);
			JTextArea descr = new JTextArea("The following items will be substituted in both subjects and comments.  You should use them to avoid flood checks.\n" +
					"\t&currentpost; is the post count, starting at 1\n" +
					"\t&finalpost; is the target post count\n" +
					"\t&captcha; is the current captcha response", 5, 60);
			descr.setEditable(false);
			descr.setBorder(new TitledBorder( new EtchedBorder(EtchedBorder.LOWERED), "Substitutions"));
			descr.setBackground(getBackground());
			add(descr);
			EmptyBorder boreder = new EmptyBorder(10, 10, 10, 10);
			JLabel subjectLabel = new JLabel("Subject");
			subjectLabel.setBorder(boreder);
			add(subjectLabel);
			subject = new JTextField("&captcha;", 35);
			subject.setBorder(boreder);
			add(subject);
			disabledWhenBusy.add(subject);
			JLabel commentLabel = new JLabel("Comment");
			commentLabel.setBorder(boreder);
			add(commentLabel);
			comment = new JTextArea("&currentpost; / &finalpost;", 10, 60);
			disabledWhenBusy.add(comment);
			JScrollPane cs = new JScrollPane(comment);
			cs.setBorder(boreder);
			add(cs);
		}
		
		public String getSubject() {
			return subject.getText();
		}
		
		public String getComment() {
			return comment.getText();
		}
	}

	private final DefaultListModel fileList = new DefaultListModel();
	private Box makeFilesTab() {
		final Box f = Box.createVerticalBox();
		final JList fileJList = new JList(fileList);
		disabledWhenBusy.add(fileJList);
		fileJList.setToolTipText("You can drag files here from other programs!");
		f.add(new JScrollPane(fileJList));
		Box buttons = Box.createHorizontalBox();
		JButton addMore = new JButton("Add files..."), remove = new JButton("Remove");
		disabledWhenBusy.add(addMore);
		disabledWhenBusy.add(remove);
		final JFileChooser chooser = new JFileChooser();
		chooser.setMultiSelectionEnabled(true);
		addMore.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (chooser.showDialog(CaptchasMoot.this, "Add files") == JFileChooser.APPROVE_OPTION) {
					for (File f : chooser.getSelectedFiles())
						fileList.addElement(f);
					startButton.setEnabled(fileList.isEmpty() == false);
				}
			}
		});
		// Drag and Drop support from other file lists
		fileJList.setDragEnabled(true);
		fileJList.setDropMode(DropMode.INSERT);
		fileJList.setTransferHandler(new TransferHandler() {
			@Override
			public boolean canImport(TransferHandler.TransferSupport info) {
				return f.isEnabled() && info.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
			}
			@Override
			public boolean importData(TransferHandler.TransferSupport info) {
				if (!info.isDrop())
					return false;
				JList.DropLocation dl = (JList.DropLocation)info.getDropLocation();
				int index = dl.getIndex();
				if (index == -1)
					index = fileJList.getModel().getSize(); // not anywhere in specific = at the endTransferable t = info.getTransferable();
                List data;
                try {
                    data = (List)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
                } catch (Exception e) { return false; }
                for (Object element : data)
                	fileList.add(index++, element);
                startButton.setEnabled(index >= 1);
                return true;
			}
		});
		remove.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				int[] sel = fileJList.getSelectedIndices();
				if (sel == null)
					return;
				for (int backward = sel.length - 1; backward >= 0; backward--)
					fileList.removeElementAt(sel[backward]);
				startButton.setEnabled(fileList.isEmpty() == false);
			}
		});
		buttons.add(Box.createGlue());
		buttons.add(addMore);
		buttons.add(Box.createGlue());
		buttons.add(remove);
		buttons.add(Box.createGlue());
		f.add(buttons);
		return f;
	}
	private List<File> getFiles() {
		List<File> files = new LinkedList<File>();
		Enumeration<?> e = fileList.elements();
		while (e.hasMoreElements())
			files.add((File)e.nextElement());
		return files;
	}

	private void connectEvents() {
		startButton.addActionListener(new StartButtonActions());
		cancelButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				poster.stop();
			}
		});
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				// Attempting to close the window will be like hitting the cancel button if something's going on.
				if (startButton.getText() == StartButtonActions.START || startButton.getText() == StartButtonActions.RESUME)
					System.exit(0);
				else
					poster.stop();
			}
		});
		postDelay.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				try {
					if (startButton.getText() != StartButtonActions.START)
						poster.setPostDelay(Double.valueOf(postDelay.getText()));
				} catch (NumberFormatException nfe) {}
			}
		});
	}
	
	private class StartButtonActions implements ActionListener {
		static final String START = "Start",
			PAUSE = "Pause",
			PAUSING = "Pausing...",
			RESUME = "Resume"; // The various states/text values.
		@Override
		public void actionPerformed(ActionEvent e) {
			if (startButton.getText() == START)
				start();
			else if (startButton.getText() == PAUSE)
				pause();
			else if (startButton.getText() == RESUME)
				resume();
		}
	}

	private boolean userAgentWarning = true;
	private void start() {
		URL address;
		try {
			address = new URL(boardOrThreadURL.getText());
		} catch (MalformedURLException m) {
			error("Input a board or thread to post to.");
			JOptionPane.showMessageDialog(this, "Must enter a valid 4chan board or thread URL.", "Invalid post address", JOptionPane.ERROR_MESSAGE);
			return;
		}
		if (userAgentWarning && userAgent.getText().equals("")) {
			JOptionPane.showMessageDialog(this, "Are you CERTAIN you don't want to override the HTTP User-Agent?\nYou may hit \"Start\" again to proceed anyway.", "User-Agent has not been set!", JOptionPane.WARNING_MESSAGE);
			userAgentWarning = false;
			return;
		}
		startButton.setText(StartButtonActions.PAUSE);
		for (Component c : disabledWhenBusy)
			c.setEnabled(false);
		cancelButton.setEnabled(true);
		progressBar.setValue(0);
		progressBar.setMaximum(fileList.getSize());
		fileNumber = 1;
		maxFileNumber = fileList.getSize();
		Double delay;
		try {
			delay = Double.parseDouble(postDelay.getText());
		} catch (NumberFormatException nfe) {
			delay = null;
		}
		poster = new Poster(this, scroller, getFiles(), address, userAgent.getText(), name.getText(), email.getText(), password.getText(), firstPost.getSubject(), firstPost.getComment(), laterPosts.getSubject(), laterPosts.getComment(), delay);
	}
	int fileNumber, maxFileNumber;

	private void pause() {
		startButton.setText(StartButtonActions.PAUSING);
		startButton.setEnabled(false);
		poster.pause();
	}
	
	@Override
	public void halted(String latestResponse) {
		startButton.setText(StartButtonActions.RESUME);
		startButton.setEnabled(true);
		if (latestResponse == null)
			info("Posting has been paused.");
		else
			error("Halted due to failure response.", latestResponse);
	}
	
	/**
	 * Log an error and detailed reasoning.
	 */
	private void error(String message, String latestResponse) {
		performLogging(LogLevel.error, message, latestResponse);
	}
	
	/**
	 * Log a warning message.
	 */
	private void warn(String message) {
		performLogging(LogLevel.warning, message, null);
	}
	
	/**
	 * Log an error with no extra details.
	 */
	private void error(String message) {
		error(message, null);
	}

	/**
	 * Log an informative message.
	 */
	private void info(String message) {
		performLogging(LogLevel.info, message, null);
	}
	
	private enum LogLevel {
		warning,
		error,
		info
	}
	private static final SimpleAttributeSet errorStyle = new SimpleAttributeSet(), detailsStyle = new SimpleAttributeSet();
	static {
		errorStyle.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.TRUE);
		detailsStyle.addAttribute(StyleConstants.CharacterConstants.FontSize, new Integer(10));
	}
	int logOffset = 0;
	private void performLogging(LogLevel level, String message, String details) {
		String time = DateFormat.getTimeInstance().format(new Date(System.currentTimeMillis())) + " (";
		if (logOffset != 0)
			time = "\n" + time;
		try {
			log.insertString(logOffset, time, null);
			logOffset += time.length();
			log.insertString(logOffset, level.toString(), level == LogLevel.error ? errorStyle : null);
			logOffset += level.toString().length();
			log.insertString(logOffset, ") " + message, null);
			logOffset += message.length() + 2;
			if (details != null) {
				log.insertString(logOffset, "\n" + details, detailsStyle);
				logOffset += details.length() + 1;
			}
		} catch (BadLocationException e) {
			// eh
		}
	}

	private void resume() {
		startButton.setText(StartButtonActions.PAUSE);
		poster.resume();
	}
	
	@Override
	public void done() {
		startButton.setText(StartButtonActions.START);
		startButton.setEnabled(getFiles().isEmpty() == false);
		for (Component c : disabledWhenBusy)
			c.setEnabled(true);
		cancelButton.setEnabled(false);
		info("Done posting.");
	}


	@Override
	public void filePostAttempt(File f) {
		info("Attempting to post " + f.getName() + "...");
	}

	@Override
	public void filePostAttempted(File f, Result result) {
		warn("File post failed temporarily: " + result);
	}
	
	@Override
	public void filePostDone(File f, Result result) {
		if (result == Result.Success)
			info("Successfully posted file " + fileNumber + " of " + maxFileNumber + ".");
		else
			error("Failed to post file " + fileNumber + " of " + maxFileNumber + ": " + result);
		progressBar.setValue(fileNumber);
		fileNumber++;
		fileList.removeElement(f);
	}
	
	public static void main(String args[]) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				CaptchasMoot ui = new CaptchasMoot();
				ui.pack();
				ui.setVisible(true);
			}
		});
	}

	@Override
	public void enteredMessageThread(String threadNumber, boolean createdAnew) {
		if (createdAnew)
			info("Created thread number " + threadNumber + ".");
		else
			info("Posting to thread number " + threadNumber + ".");
	}
}
